﻿using System;
using System.IO;
using System.Net;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

using static System.Text.Encoding;

using NullGuard;

public static class HttpWebRequestAsyncEx
{
#pragma warning disable 1591

    public static int DefaultBufferSize { get; set; } = 81920;
    public const int MinimumBufferSize = 4096;

    #region simple extension methods

    public static async Task<HttpWebResponse> GetAsync(this HttpWebRequest request)
    {
        request.Method = "GET";
        return (HttpWebResponse)await request.GetResponseAsync();
    }

    public static async Task<string> GetResponseTextAsync(this HttpWebResponse response, Encoding encoding = null)
    {
        using (var stream = response.GetResponseStream())
        using (var reader = new StreamReader(stream, encoding ?? Encoding.UTF8))
            return await reader.ReadToEndAsync();
    }
    public static async Task<string> GetResponseTextAsync(this HttpWebRequest request, Encoding encoding = null)
    {
        using (var response = (HttpWebResponse)await request.GetResponseAsync())
            return await response.GetResponseTextAsync(encoding);
    }

    public static async Task<HttpWebResponse> PostAsync(this HttpWebRequest request, [AllowNull]Stream stream, int bufferSize, CancellationToken cancellationToken)
    {
        cancellationToken.ThrowIfCancellationRequested();

        if (bufferSize < MinimumBufferSize)
            bufferSize = MinimumBufferSize;

        request.Method = "POST";
        using (var requestStream = await request.GetRequestStreamAsync())
        {
            if (stream != null)
                await stream.CopyToAsync(requestStream, bufferSize, cancellationToken);
        }
        return (HttpWebResponse)await request.GetResponseAsync();
    }
    public static Task<HttpWebResponse> PostAsync(this HttpWebRequest request, [AllowNull]Stream stream, CancellationToken cancellationToken)
        => request.PostAsync(stream, DefaultBufferSize, cancellationToken);
    public static Task<HttpWebResponse> PostAsync(this HttpWebRequest request, [AllowNull]Stream stream, int bufferSize)
        => request.PostAsync(stream, bufferSize, CancellationToken.None);
    public static Task<HttpWebResponse> PostAsync(this HttpWebRequest request, [AllowNull]Stream stream)
        => request.PostAsync(stream, DefaultBufferSize, CancellationToken.None);

    public static async Task<HttpWebResponse> PostAsync(this HttpWebRequest request, byte[] data = null, bool makeCopy = false)
    {
        if (makeCopy && data != null)
            data = (byte[])data.Clone();

        request.Method = "POST";
        using (var stream = await request.GetRequestStreamAsync())
        {
            if (data != null)
                await stream.WriteAsync(data, 0, data.Length);
        }
        return (HttpWebResponse)await request.GetResponseAsync();
    }

    public static async Task<HttpWebResponse> PostAsync(this HttpWebRequest request, [AllowNull]string content, Encoding encoding = null)
    {
        return await request.PostAsync((encoding ?? Encoding.UTF8).GetBytes(content ?? ""));
    }

    #endregion

    #region multipart extension methods

    private static byte[] NewLineBytes = Encoding.ASCII.GetBytes("\r\n");

    public static async Task WriteMultipartFormFieldAsync(this Stream stream, string boundary, string name, string value, CancellationToken cancellationToken)
    {
        cancellationToken.ThrowIfCancellationRequested();

        var s = $"--{boundary}\r\nContent-Disposition: form-data; name=\"{Uri.EscapeDataString(name)}\"\r\n\r\n{value}\r\n";
        var bytes = ASCII.GetBytes(s);
        await stream.WriteAsync(bytes, 0, bytes.Length, cancellationToken);
    }
    public static Task WriteMultipartFormFieldAsync(this Stream stream, string boundary, string name, string value)
        => stream.WriteMultipartFormFieldAsync(boundary, name, value, CancellationToken.None);

    public static async Task WriteMultipartFileFieldAsync(this Stream target, string boundary, string name, string filename, Stream source, int bufferSize, CancellationToken cancellationToken)
    {
        cancellationToken.ThrowIfCancellationRequested();

        if (bufferSize < MinimumBufferSize)
            bufferSize = MinimumBufferSize;

        var s = $"--{boundary}\r\n";
        s += $"Content-Disposition: form-data; name=\"{Uri.EscapeDataString(name)}\"; ";
        s += $"filename=\"{Uri.EscapeDataString(filename)}\"\r\n";
        s += "Content-Type: application/octet-stream\r\n\r\n";

        var bytes = ASCII.GetBytes(s);
        await target.WriteAsync(bytes, 0, bytes.Length, cancellationToken);

        await source.CopyToAsync(target, bufferSize, cancellationToken);

        bytes = NewLineBytes;
        await target.WriteAsync(bytes, 0, bytes.Length, cancellationToken);
    }
    public static Task WriteMultipartFileFieldAsync(this Stream target, string boundary, string name, string filename, Stream source, CancellationToken cancellationToken)
        => target.WriteMultipartFileFieldAsync(boundary, name, filename, source, DefaultBufferSize, cancellationToken);
    public static Task WriteMultipartFileFieldAsync(this Stream target, string boundary, string name, string filename, Stream source, int bufferSize)
        => target.WriteMultipartFileFieldAsync(boundary, name, filename, source, bufferSize, CancellationToken.None);
    public static Task WriteMultipartFileFieldAsync(this Stream target, string boundary, string name, string filename, Stream source)
        => target.WriteMultipartFileFieldAsync(boundary, name, filename, source, DefaultBufferSize, CancellationToken.None);

    public static async Task WriteMultipartFileFieldAsync(this Stream target, string boundary, string name, string path, int bufferSize, CancellationToken cancellationToken)
    {
        cancellationToken.ThrowIfCancellationRequested();

        using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read))
            await target.WriteMultipartFileFieldAsync(boundary, name, Path.GetFileName(path), fs, bufferSize, cancellationToken);
    }
    public static Task WriteMultipartFileFieldAsync(this Stream target, string boundary, string name, string path, CancellationToken cancellationToken)
        => target.WriteMultipartFileFieldAsync(boundary, name, path, DefaultBufferSize, cancellationToken);
    public static Task WriteMultipartFileFieldAsync(this Stream target, string boundary, string name, string path, int bufferSize)
        => target.WriteMultipartFileFieldAsync(boundary, name, path, bufferSize, CancellationToken.None);
    public static Task WriteMultipartFileFieldAsync(this Stream target, string boundary, string name, string path)
        => target.WriteMultipartFileFieldAsync(boundary, name, path, DefaultBufferSize, CancellationToken.None);


    public static async Task WriteMultipartFileFieldAsync(this Stream target, string boundary, string name, string filename, Stream source, long length, int bufferSize, CancellationToken cancellationToken)
    {
        cancellationToken.ThrowIfCancellationRequested();

        if (length < 0)
        {
            await target.WriteMultipartFileFieldAsync(boundary, name, filename, source, bufferSize, cancellationToken);
            return;
        }

        if (bufferSize < MinimumBufferSize)
            bufferSize = MinimumBufferSize;

        var s = $"--{boundary}\r\n";
        s += $"Content-Disposition: form-data; name=\"{Uri.EscapeDataString(name)}\"; ";
        s += $"filename=\"{Uri.EscapeDataString(filename)}\"\r\n";
        s += "Content-Type: application/octet-stream\r\n\r\n";

        var bytes = ASCII.GetBytes(s);
        await target.WriteAsync(bytes, 0, bytes.Length, cancellationToken);

        bytes = new byte[bufferSize];
        int read;
        long left = length;
        do
        {
            if (left == 0) break; // end of range

            if (left < bufferSize)
                read = (int)left;
            else
                read = bufferSize;

            read = await source.ReadAsync(bytes, 0, read, cancellationToken);
            await target.WriteAsync(bytes, 0, read, cancellationToken);

            left -= read;
        } while (read == bufferSize); // end of file or range

        bytes = NewLineBytes;
        await target.WriteAsync(bytes, 0, bytes.Length, cancellationToken);
    }
    public static Task WriteMultipartFileFieldAsync(this Stream target, string boundary, string name, string filename, Stream source, long length, CancellationToken cancellationToken)
        => target.WriteMultipartFileFieldAsync(boundary, name, filename, source, length, DefaultBufferSize, cancellationToken);
    public static Task WriteMultipartFileFieldAsync(this Stream target, string boundary, string name, string filename, Stream source, long length, int bufferSize)
        => target.WriteMultipartFileFieldAsync(boundary, name, filename, source, length, bufferSize, CancellationToken.None);
    public static Task WriteMultipartFileFieldAsync(this Stream target, string boundary, string name, string filename, Stream source, long length)
        => target.WriteMultipartFileFieldAsync(boundary, name, filename, source, length, DefaultBufferSize, CancellationToken.None);

    public static async Task WriteMultipartFileFieldAsync(this Stream target, string boundary, string name, string path, long length, int bufferSize, CancellationToken cancellationToken)
    {
        cancellationToken.ThrowIfCancellationRequested();

        if (length < 0)
        {
            await target.WriteMultipartFileFieldAsync(boundary, name, path, bufferSize, cancellationToken);
            return;
        }

        using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read))
            await target.WriteMultipartFileFieldAsync(boundary, name, Path.GetFileName(path), fs, length, bufferSize, cancellationToken);
    }
    public static Task WriteMultipartFileFieldAsync(this Stream target, string boundary, string name, string path, long length, CancellationToken cancellationToken)
        => target.WriteMultipartFileFieldAsync(boundary, name, path, length, DefaultBufferSize, cancellationToken);
    public static Task WriteMultipartFileFieldAsync(this Stream target, string boundary, string name, string path, long length, int bufferSize)
        => target.WriteMultipartFileFieldAsync(boundary, name, path, length, bufferSize, CancellationToken.None);
    public static Task WriteMultipartFileFieldAsync(this Stream target, string boundary, string name, string path, long length)
        => target.WriteMultipartFileFieldAsync(boundary, name, path, length, DefaultBufferSize, CancellationToken.None);

    #endregion
}
